Skip to content

fix: search input initialization and re-focus bug#2148

Open
nnecec wants to merge 3 commits intonpmx-dev:mainfrom
nnecec:fix/home-search-input
Open

fix: search input initialization and re-focus bug#2148
nnecec wants to merge 3 commits intonpmx-dev:mainfrom
nnecec:fix/home-search-input

Conversation

@nnecec
Copy link
Copy Markdown

@nnecec nnecec commented Mar 19, 2026

🔗 Linked issue

no issue

🧭 Context

no context

📚 Description

Fixed with Cursor, and reviewed by me.

3.19.1.mp4

Fixed two issues:

  1. After auto-focus on homepage, rapidly typed characters were being reset.
  2. Auto-focus was not triggered when redirect to homepage via logo click from other pages.

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview, Comment Apr 21, 2026 6:11am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Apr 21, 2026 6:11am
npmx-lunaria Ignored Ignored Apr 21, 2026 6:11am

Request Review

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 19, 2026

Codecov Report

❌ Patch coverage is 38.88889% with 22 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
app/composables/useGlobalSearch.ts 38.88% 14 Missing and 8 partials ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 69e8f103-5891-4b39-bba0-172b0531aec4

📥 Commits

Reviewing files that changed from the base of the PR and between 432500b and c4f5762.

📒 Files selected for processing (1)
  • app/composables/useGlobalSearch.ts

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Better preservation of in-progress typing when navigating between pages.
    • Search state now restores immediately after hydration if the focused search input contains text.
    • URL↔search synchronisation limited to the search page to avoid unwanted overwrites.
    • Homepage navigation clears search and focuses the homepage search field after render.

Walkthrough

Updates to app/composables/useGlobalSearch.ts that preserve client-side focused search input values across hydration and route changes, restrict URL→state syncing to the search route, add client-only homepage focus/clear behaviour, and perform hydration recovery from the focused input. (Lines changed: +67/−5)

Changes

Cohort / File(s) Summary
Composable
app/composables/useGlobalSearch.ts
Add client-only helper getFocusedSearchInputValue; initialize searchQuery from focused input on client; watcher now depends on [route.name, route.query.q] and updates only when route.name === 'search'; skip clobbering when input focused; only assign when URL value differs; clear/commit and focus #home-search on navigating to index; add onMounted hydration recovery.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant DOM as "Browser / focused <input>"
    participant Composable as "useGlobalSearch"
    participant Router as "Route / query"
    participant Render as "nextTick / Render"

    User->>DOM: focus / type in search input
    DOM->>Composable: getFocusedSearchInputValue() (client-only)
    Composable->>Composable: initialise searchQuery from focused input (client)
    Note over Router,Composable: watcher triggers on [route.name, route.query.q]
    Router->>Composable: route.name changes
    alt route.name === 'search' and no input focused
        Router->>Composable: apply normalized query → searchQuery (only if different)
    else input is focused
        Composable-->>Router: skip clobbering searchQuery
    end
    opt route.name becomes 'index' (client)
        Composable->>Composable: clear searchQuery & committedSearchQuery
        Composable->>Render: nextTick
        Render->>DOM: focus and select `#home-search`
    end
    Note over DOM,Composable: onMounted hydration: if searchQuery empty and focused input has value, set via computed setter to restore instant-search.
Loading
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarises the main changes: fixing search input initialization and re-focus bugs on the homepage.
Description check ✅ Passed The description is related to the changeset, explaining the two specific bugs fixed and providing context about AI tool usage and manual review.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
app/composables/useGlobalSearch.ts (1)

60-67: Consider extracting focused input detection into a reusable helper.

The logic for detecting whether a search input is focused (lines 60-67) duplicates the pattern from getFocusedSearchInputValue (lines 22-24). Extracting a small isSearchInputFocused() helper would improve maintainability.

♻️ Proposed refactor
+const isSearchInputFocused = (): boolean => {
+  if (!import.meta.client) return false
+  const active = document.activeElement
+  if (!(active instanceof HTMLInputElement)) return false
+  return active.type === 'search' || active.name === 'q'
+}
+
 const getFocusedSearchInputValue = () => {
-  if (!import.meta.client) return ''
-
-  const active = document.activeElement
-  if (!(active instanceof HTMLInputElement)) return ''
-  if (active.type !== 'search' && active.name !== 'q') return ''
-  return active.value
+  if (!isSearchInputFocused()) return ''
+  return (document.activeElement as HTMLInputElement).value
 }

Then in the watcher:

-     if (import.meta.client) {
-       const active = document.activeElement
-       if (
-         active instanceof HTMLInputElement &&
-         (active.type === 'search' || active.name === 'q')
-       ) {
-         return
-       }
-     }
+     if (isSearchInputFocused()) return

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ecd4a617-b5c7-4a12-93b5-29da7a115b93

📥 Commits

Reviewing files that changed from the base of the PR and between 5d8fcf5 and 399f570.

📒 Files selected for processing (1)
  • app/composables/useGlobalSearch.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6ac65a2-c31a-42f4-80dc-a8f836ca6980

📥 Commits

Reviewing files that changed from the base of the PR and between 399f570 and 4636625.

📒 Files selected for processing (1)
  • app/composables/useGlobalSearch.ts

Comment thread app/composables/useGlobalSearch.ts
Comment thread app/composables/useGlobalSearch.ts
@serhalp serhalp added the needs review This PR is waiting for a review from a maintainer label Mar 21, 2026
Copy link
Copy Markdown
Contributor

@graphieros graphieros left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙏 Can you please address the code rabbit comments ?

@serhalp serhalp added stale This has become stale and may be closed soon and removed needs review This PR is waiting for a review from a maintainer labels Apr 10, 2026
@github-actions github-actions Bot removed the stale This has become stale and may be closed soon label Apr 18, 2026
@serhalp serhalp added the question Further information is requested label Apr 20, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
app/composables/useGlobalSearch.ts (2)

65-73: ⚠️ Potential issue | 🟠 Major

Do not permanently drop focused URL synchronisation.

This early return also ignores legitimate /search?q=... history or programmatic changes while the input stays focused, leaving searchQuery and committedSearchQuery stale. Please distinguish self-initiated pending URL updates from external route changes, or queue a resync when the focused input blurs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useGlobalSearch.ts` around lines 65 - 73, The current early
return in useGlobalSearch that checks document.activeElement prevents handling
legitimate external route/query changes; instead of unconditionally returning in
the focused-input branch, only suppress updates that are self-initiated (e.g.
compare the incoming route.query.q to a pending/last-synced value or to
committedSearchQuery) and allow external changes to update searchQuery and
committedSearchQuery; if the input is focused and the change is external, queue
a resync to apply the new query on blur by attaching a one-time blur handler
that will set searchQuery/committedSearchQuery to the route q, and add a small
pending flag (or lastSyncedQuery) to differentiate programmatic/self updates
from external route changes.

141-174: ⚠️ Potential issue | 🟠 Major

Guard mounted recovery after an intentional home reset.

The /search/ reset can be undone if onMounted reads the still-focused header input before the cleared model reaches the DOM. Add a one-shot shared guard so the same navigation that clears the home search cannot immediately restore the stale query.

Suggested guard shape
+  const skipFocusedRecovery = useState('skip-focused-search-recovery', () => false)
+
   if (import.meta.client) {
     watch(
       () => route.name,
       name => {
         if (name !== 'index') return
+        skipFocusedRecovery.value = true
         searchQuery.value = ''
         committedSearchQuery.value = ''
         // Use nextTick so we run after the homepage has rendered.
         nextTick(() => {
@@
   if (import.meta.client) {
     onMounted(() => {
+      if (skipFocusedRecovery.value && route.name === 'index') {
+        skipFocusedRecovery.value = false
+        return
+      }
+
       const focusedInputValue = getFocusedSearchInputValue()
       if (!focusedInputValue) return
       if (searchQuery.value) return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useGlobalSearch.ts` around lines 141 - 174, Introduce a
one-shot guard ref (e.g., recentlyResetHome) shared between the route watch
block and onMounted so the intentional home reset cannot be immediately undone:
create a ref flag (recentlyResetHome) default false, set it to true inside the
watch handler right when you clear searchQuery/committedSearchQuery (before
calling nextTick), and in the onMounted block check if recentlyResetHome.value
is true — if so clear the flag and return early instead of restoring from
getFocusedSearchInputValue; optionally clear the flag after the nextTick in the
watch so it only prevents a single immediate restoration.
🧹 Nitpick comments (1)
app/composables/useGlobalSearch.ts (1)

10-183: Split the client-only search lifecycle concerns.

useGlobalSearch now owns provider selection, URL sync, debounce control, homepage focus reset, and hydration recovery in one large function. Consider extracting the homepage reset and hydration recovery watchers into small helpers to reduce future state-sync regressions. As per coding guidelines, “Keep functions focused and manageable (generally under 50 lines)”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useGlobalSearch.ts` around lines 10 - 183, The
useGlobalSearch function is doing too much; extract the homepage-reset watcher
and the hydration recovery onMounted logic into two small helper functions to
keep responsibilities separated: create (1) resetHomeSearchOnNavigate which
contains the watch on route.name that clears searchQuery and
committedSearchQuery and focuses/selects the `#home-search` input (using nextTick)
and is registered only when import.meta.client, and (2)
recoverFocusedInputOnMount which contains the onMounted block that reads
getFocusedSearchInputValue and sets searchQueryValue when appropriate; call both
helpers from useGlobalSearch in place of the inlined blocks and keep all
existing uses of searchQuery, committedSearchQuery, searchQueryValue,
getFocusedSearchInputValue, and flushUpdateUrlQuery unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/composables/useGlobalSearch.ts`:
- Around line 21-28: getFocusedSearchInputValue currently returns any focused
search or name="q" input which can capture page-local filters; tighten it to
only return a value for inputs that are explicitly meant to seed the global
search (e.g. check for a marker attribute/class such as data-global-search or
class "global-search-input") and ensure the global search initializer in
useGlobalSearch runs the pagesWithLocalFilter guard before reading any
focused/input value. Update all occurrences of getFocusedSearchInputValue and
the initializer/restore logic in useGlobalSearch (including the other similar
blocks) to use the attribute/class check and to defer reading until after
pagesWithLocalFilter has been checked so local page ?q filters are never
imported into the shared global state.

---

Duplicate comments:
In `@app/composables/useGlobalSearch.ts`:
- Around line 65-73: The current early return in useGlobalSearch that checks
document.activeElement prevents handling legitimate external route/query
changes; instead of unconditionally returning in the focused-input branch, only
suppress updates that are self-initiated (e.g. compare the incoming
route.query.q to a pending/last-synced value or to committedSearchQuery) and
allow external changes to update searchQuery and committedSearchQuery; if the
input is focused and the change is external, queue a resync to apply the new
query on blur by attaching a one-time blur handler that will set
searchQuery/committedSearchQuery to the route q, and add a small pending flag
(or lastSyncedQuery) to differentiate programmatic/self updates from external
route changes.
- Around line 141-174: Introduce a one-shot guard ref (e.g., recentlyResetHome)
shared between the route watch block and onMounted so the intentional home reset
cannot be immediately undone: create a ref flag (recentlyResetHome) default
false, set it to true inside the watch handler right when you clear
searchQuery/committedSearchQuery (before calling nextTick), and in the onMounted
block check if recentlyResetHome.value is true — if so clear the flag and return
early instead of restoring from getFocusedSearchInputValue; optionally clear the
flag after the nextTick in the watch so it only prevents a single immediate
restoration.

---

Nitpick comments:
In `@app/composables/useGlobalSearch.ts`:
- Around line 10-183: The useGlobalSearch function is doing too much; extract
the homepage-reset watcher and the hydration recovery onMounted logic into two
small helper functions to keep responsibilities separated: create (1)
resetHomeSearchOnNavigate which contains the watch on route.name that clears
searchQuery and committedSearchQuery and focuses/selects the `#home-search` input
(using nextTick) and is registered only when import.meta.client, and (2)
recoverFocusedInputOnMount which contains the onMounted block that reads
getFocusedSearchInputValue and sets searchQueryValue when appropriate; call both
helpers from useGlobalSearch in place of the inlined blocks and keep all
existing uses of searchQuery, committedSearchQuery, searchQueryValue,
getFocusedSearchInputValue, and flushUpdateUrlQuery unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 25c3d4b8-e75a-43ae-bfc6-b6fc452f845b

📥 Commits

Reviewing files that changed from the base of the PR and between 4636625 and 432500b.

📒 Files selected for processing (1)
  • app/composables/useGlobalSearch.ts

Comment thread app/composables/useGlobalSearch.ts Outdated
Comment on lines +21 to +28
const getFocusedSearchInputValue = () => {
if (!import.meta.client) return ''

const active = document.activeElement
if (!(active instanceof HTMLInputElement)) return ''
if (active.type !== 'search' && active.name !== 'q') return ''
return active.value
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Do not recover page-local filters as global search input.

getFocusedSearchInputValue() currently accepts any focused search input or name="q" input, and the initialiser reads it before the pagesWithLocalFilter guard. On pages with local ?q filters, that can leak local filter text into the shared global search state.

Suggested tightening
   const getFocusedSearchInputValue = () => {
     if (!import.meta.client) return ''

     const active = document.activeElement
     if (!(active instanceof HTMLInputElement)) return ''
+    if (active.id !== 'home-search' && active.id !== 'header-search') return ''
     if (active.type !== 'search' && active.name !== 'q') return ''
     return active.value
   }

Also applies to: 31-38, 167-173

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/composables/useGlobalSearch.ts` around lines 21 - 28,
getFocusedSearchInputValue currently returns any focused search or name="q"
input which can capture page-local filters; tighten it to only return a value
for inputs that are explicitly meant to seed the global search (e.g. check for a
marker attribute/class such as data-global-search or class
"global-search-input") and ensure the global search initializer in
useGlobalSearch runs the pagesWithLocalFilter guard before reading any
focused/input value. Update all occurrences of getFocusedSearchInputValue and
the initializer/restore logic in useGlobalSearch (including the other similar
blocks) to use the attribute/class check and to defer reading until after
pagesWithLocalFilter has been checked so local page ?q filters are never
imported into the shared global state.

…ve search query handling

- Moved getFocusedSearchInputValue function to a more appropriate location.
- Simplified logic for checking active input element and its type.
- Enhanced condition to prevent overwriting search query when focused input matches URL value.
- Cleaned up formatting for better readability.
@nnecec
Copy link
Copy Markdown
Author

nnecec commented Apr 21, 2026

🙏 Can you please address the code rabbit comments ?

Sorry for missing the comments. I just fixed the question according to coderabbitai's comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

question Further information is requested

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants